﻿namespace Home.Security.Authentication
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Configuration;
    using System.Data.Entity;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;
    using Home.Configuration;
    using Home.Data;
    using Home.Http;
    using Home.Security;
    using Home.Security.Authentication.IdentityProviders;
    using Home.Security.Authentication.Repository;

    // TODO: we need a job to cleanup expired authentication in database
    public static class AuthenticationHelper
    {
        public static Task<string> GenerateLoginUriAsync(AuthenticationType type)
        {
            Debug.Assert(Enum.IsDefined(typeof(AuthenticationType), type));

            var provider = IdentityProviderFactory.Create(type);
            var result = provider.GenerateLoginUriAsync();
            return result;
        }

        public static async Task<AuthorizationCodeResult> GetAuthorizationCodeAsync(string callbackUri)
        {
            Debug.Assert(!string.IsNullOrEmpty(callbackUri));

            var codeResult = default(string);
            var statusResult = default(AuthenticationStatus);
            var queries = default(NameValueCollection);
            var parameter = default(AuthenticationParameter);

            // get URI query string
            {
                var uri = new Uri(callbackUri, UriKind.Absolute);
                var query = uri.Query;

                if (string.IsNullOrEmpty(query))
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }

                if (query[0] == '?')
                {
                    query = query.Substring(1);
                }

                if (string.IsNullOrEmpty(query))
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }

                queries = HttpUtility.ParseQueryString(query);
            }

            // get state
            {
                var state = queries["state"];
                if (string.IsNullOrEmpty(state))
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }

                try
                {
                    parameter = SecurityHelper.UnprotectData<AuthenticationParameter>(state);
                }
                catch (Exception)
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }
            }

            // validate the state
            {
                if (!Enum.IsDefined(typeof(AuthenticationType), parameter.AuthenticationType))
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }

                var now = DateTimeOffset.UtcNow;
                if (parameter.TimeStamp >= now)
                {
                    statusResult = AuthenticationStatus.Invalid;
                    goto EXIT;
                }

                var interval = now - parameter.TimeStamp;
                if (interval.TotalMinutes > ConfigurationHelper.MaxLoginTime.TotalMinutes)
                {
                    statusResult = AuthenticationStatus.Timeout;
                    goto EXIT;
                }
            }

            // get authorization code
            {
                var provider = IdentityProviderFactory.Create(parameter.AuthenticationType);
                var code = await provider.GetAuthorizationCodeAsync(parameter, queries);
                if (string.IsNullOrEmpty(code))
                {
                    statusResult = AuthenticationStatus.Rejected;
                    goto EXIT;
                }

                codeResult = code;
                statusResult = AuthenticationStatus.Passed;
            }

            EXIT:

            var result = default(AuthorizationCodeResult);

            if (parameter != null)
            {
                result = new AuthorizationCodeResult(parameter)
                {
                    CallbackUri = callbackUri,
                    AuthorizationCode = codeResult,
                    AuthenticationStatus = statusResult,
                };
            }

            return result;
        }

        public static async Task<AccessTokenResult> GetAccessTokenAsync(AuthorizationCodeResult authorization)
        {
            Debug.Assert(authorization != null);

            var provider = IdentityProviderFactory.Create(authorization.AuthenticationType);
            //// get access token via authorization code
            var uri = await provider.GenerateAccessTokenRequestUriAsync(authorization);
            var response = await HttpHelper.GetStringAsync(uri);
            if (response == null)
            {
                return null;
            }
            var items = provider.ParseHttpResponse(response.Body);
            var result = await provider.ParseAccessTokenResponseAsync(authorization, items);
            //// new an authentication object
            var authentication = new AuthenticationEntity()
            {
                Id = result.Id,
                TrackId = result.TrackId,
                AuthenticationType = result.AuthenticationType,
                AuthenticationUri = await provider.GenerateLoginUriAsync(),
                AuthenticationDateTime = authorization.AuthenticationParameter.TimeStamp,
                AuthorizationCallbackUri = authorization.CallbackUri,
                AuthorizationCode = authorization.AuthorizationCode,
                AuthorizationStatus = authorization.AuthenticationStatus,
                AccessTokenUri = uri,
                AccessToken = result.AccessToken,
                RefreshToken = result.RefreshToken,
                AccessTokenResponseHeader = response.Header,
                AccessTokenResponseBody = response.Body,
            };
            foreach (var item in result.Properties)
            {
                authentication.AccessTokenProperties.Add(item.Key, item.Value);
            }

            //// get SNS info from IP
            var sns = await GetSnsResultAsync(authentication, provider);
            if (sns == null)
            {
                return null;
            }

            var dbNow = DateTimeOffset.UtcNow;
            var dbUser = Thread.CurrentPrincipal.Identity.Name;
            using (var database = new AuthenticationDbContext(true))
            {
                // create authentication in database
                authentication.Create(database.Authentications, dbNow, dbUser, true);

                // get user from database
                // we get user via open ID or others
                var user = await GetApplicationUserEntityAsync(provider, authentication, database);
                if (user == null)
                {
                    user = new ApplicationUserEntity();
                    user.Id = Guid.NewGuid();
                    user.Key = await provider.GetUserKeyAsync(authentication);
                    user.AuthenticationType = authentication.AuthenticationType;
                    user.Create(database.Users, dbNow, dbUser, true);
                }

                result.UserId = user.Id;
                //// update user's SNS info in database
                await UpdateApplicationUserSnsAsync(provider, user, sns);

                await database.SaveChangesAsync();
            }

            return result;
        }

        public static async Task<bool> CheckAuthenticationStatus(Guid id)
        {
            Debug.Assert(id != Guid.Empty);

            var authentication = await GetAuthenticationEntityAsync(id);
            if (authentication == null)
            {
                return false;
            }

            var provider = IdentityProviderFactory.Create(authentication.AuthenticationType);
            var uri = await provider.GenerateVerifyAccessTokenRequestUriAsync(authentication);
            var response = await HttpHelper.GetStringAsync(uri);
            if (response == null)
            {
                return false;
            }
            var items = provider.ParseHttpResponse(response.Body);
            var result = await provider.ParseVerifyAccessTokenResponseAsync(authentication, items);

            return result.IsValid;
        }

        public static async Task<RefreshTokenResult> GetRefreshTokenAsync(Guid id)
        {
            Debug.Assert(id != Guid.Empty);

            var authentication = await GetAuthenticationEntityAsync(id);
            if (authentication == null)
            {
                return null;
            }

            var provider = IdentityProviderFactory.Create(authentication.AuthenticationType);
            //// refresh token via IP
            var uri = await provider.GenerateRefreshTokenRequestUriAsync(authentication);
            var response = await HttpHelper.GetStringAsync(uri);
            if (response == null)
            {
                return null;
            }
            var items = provider.ParseHttpResponse(response.Body);
            var result = await provider.ParseRefreshTokenResponseAsync(authentication, items);
            //// update authentication object
            authentication.AccessToken = result.AccessToken;
            authentication.RefreshToken = result.RefreshToken;
            authentication.RefreshTokenUri = uri;
            authentication.RefreshTokenResponseHeader = response.Header;
            authentication.RefreshTokenResponseBody = response.Body;
            foreach (var item in result.Properties)
            {
                authentication.RefreshTokenProperties.Add(item.Key, item.Value);
            }

            //// get SNS info from IP
            var sns = await GetSnsResultAsync(authentication, provider);
            if (sns == null)
            {
                return null;
            }

            var dbNow = DateTimeOffset.UtcNow;
            var dbUser = Thread.CurrentPrincipal.Identity.Name;
            using (var database = new AuthenticationDbContext(true))
            {
                // update authentication in database
                authentication.Update(database, database.Authentications, dbNow, dbUser);

                // get user from database
                // we get user via open ID or others
                var user = await GetApplicationUserEntityAsync(provider, authentication, database);
                if (user == null)
                {
                    // the user does not exist in database
                    //  1. the user data might be lost
                    //  2. this might be an security attack
                    return null;
                }
                else
                {
                    // update user's SNS info in database
                    await UpdateApplicationUserSnsAsync(provider, user, sns);
                    result.UserId = user.Id;
                }

                await database.SaveChangesAsync();
            }

            return result;
        }

        private static Task<AuthenticationEntity> GetAuthenticationEntityAsync(Guid id)
        {
            using (var database = new AuthenticationDbContext(false))
            {
                return database.Authentications.FindAsync(id);
            }
        }

        private static async Task<SnsResult> GetSnsResultAsync(AuthenticationEntity authentication, IdentityProvider provider)
        {
            var uri = await provider.GenerateSnsRequestUriAsync(authentication);
            var response = await HttpHelper.GetStringAsync(uri);
            if (response == null)
            {
                return null;
            }

            var items = provider.ParseHttpResponse(response.Body);
            var result = await provider.ParseSnsResponseAsync(authentication, items);
            if (result == null)
            {
                return null;
            }

            authentication.SnsUri = uri;
            authentication.SnsResponseHeader = response.Header;
            authentication.SnsResponseBody = response.Body;
            foreach (var item in result.Properties)
            {
                authentication.SnsProperties.Add(item.Key, item.Value);
            }

            var dbNow = DateTimeOffset.UtcNow;
            var dbUser = Thread.CurrentPrincipal.Identity.Name;
            using (var database = new AuthenticationDbContext(true))
            {
                authentication.Update(database, database.Authentications, dbNow, dbUser);
                await database.SaveChangesAsync();
            }

            return result;
        }

        private static async Task<ApplicationUserEntity> GetApplicationUserEntityAsync(IdentityProvider provider, AuthenticationEntity authentication, AuthenticationDbContext database)
        {
            var key = await provider.GetUserKeyAsync(authentication);
            if (string.IsNullOrEmpty(key))
            {
                return null;
            }

            return await database.Users.FirstOrDefaultAsync(u => u.Key == key);
        }

        private static async Task UpdateApplicationUserSnsAsync(IdentityProvider provider, ApplicationUserEntity entity, SnsResult sns)
        {
            await provider.UpdateUserSnsAsync(entity, sns);
            foreach (var item in sns.Properties)
            {
                entity.SnsProperties.Add(item.Key, item.Value);
            }
        }
    }
}
